package main

import (
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path"
	"runtime/debug"
	"strings"
)

const (
	// 0x0001 即为一个双字节的数值1
	ListDir      = 0x0001
	UPLOAD_DIR   = "./uploads"
	TEMPLATE_DIR = "./views"
)

// 存放所有模板内容
var templates = make(map[string]*template.Template)

// init() 会在 main() 函数之前执行
// 对模板进行缓存，即指一次性预加载模板
func init() {
	// 读取文件
	fileInfoArr, err := ioutil.ReadDir(TEMPLATE_DIR)
	check(err)
	var templateName, templatePath string
	for _, fileInfo := range fileInfoArr {
		templateName = fileInfo.Name()
		log.Println("template name:", templateName)
		// path.Ext 返回路径中扩展
		var ext string
		if ext = path.Ext(templateName); ext != ".html" {
			continue
		}
		templatePath = TEMPLATE_DIR + "/" + templateName
		log.Println("Loading template:", templatePath)
		//  template.Must() 确保了模板不能解析成功时，一定会触发错误处理流程
		t := template.Must(template.ParseFiles(templatePath))
		tmpl := strings.TrimSuffix(templateName, ext)
		templates[tmpl] = t
	}
}

func check(err error) {
	if err != nil {
		// 当在一个函数执行过程中调用 panic() 函数时，正常的函数执行流程将立即终止，但函数中
		// 之前使用 defer 关键字延迟执行的语句将正常展开执行，之后该函数将返回到调用函数，并导致
		// 逐层向上执行 panic 流程，直至所属的goroutine中所有正在执行的函数被终止。错误信息将被报
		// 告，包括在调用 panic() 函数时传入的参数，这个过程称为错误处理流程。
		panic(err)
	}
}

// 加载html
// tmpl:html名称
func renderHtml(w http.ResponseWriter, tmpl string, locals map[string]interface{}) {
	// t.Execute()方法会根据模板语法来执行模板的渲染
	// 并将渲染后的结果作为HTTP的返回数据输出
	err := templates[tmpl].Execute(w, locals)
	check(err)
}

// 判断文件是否存在
func isExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	//	if e, ok := err.(*os.PathError); ok && e.Error == os.ENOENT {
	if _, ok := err.(*os.PathError); ok {
		return false, nil
	}
	return false, err
}

// 上传照片
func uploadHandler(w http.ResponseWriter, r *http.Request) {
	// 显示画面
	if r.Method == "GET" {
		renderHtml(w, "upload", nil)
	}
	// 上传照片
	if r.Method == "POST" {
		//从表单提交过来的字段寻找名为 image 的文件域并对其接值
		// 调用 r.FormFile() 方法会返回3个值，各个值的类型分别是 multipart.File 、*multipart.FileHeader 和 error 。
		f, h, err1 := r.FormFile("image")

		check(err1)
		filename := h.Filename
		defer f.Close()
		t, err2 := os.Create(UPLOAD_DIR + "/" + filename)
		//		t, err2 := ioutil.TempFile(UPLOAD_DIR, filename)
		check(err2)
		defer t.Close()
		_, err3 := io.Copy(t, f)
		check(err3)
		// 跳转查看图片页面
		http.Redirect(w, r, "/view?id="+filename, http.StatusFound)
	}
}

// 画面上显示照片  /view?id=<ImageId>
func viewHandler(w http.ResponseWriter, r *http.Request) {
	// 图片唯一ID
	imageId := r.FormValue("id")
	imagePath := UPLOAD_DIR + "/" + imageId
	// 判断是否存在
	exists, _ := isExists(imagePath)
	if !exists {
		http.NotFound(w, r)
		return
	}
	// HTTP响应头输出格式预设为 image
	w.Header().Set("Content-Type", "image")
	http.ServeFile(w, r, imagePath)
}

// 列出所有已上传图片
func listHandler(w http.ResponseWriter, r *http.Request) {
	// 获取文件夹中文件
	fileInfoArr, err := ioutil.ReadDir("./uploads")
	check(err)
	locals := make(map[string]interface{})
	// 切片
	images := []string{}
	// 遍历
	for _, fileInfo := range fileInfoArr {
		// 文件名
		images = append(images, fileInfo.Name())
	}
	locals["images"] = images
	renderHtml(w, "list", locals)
}

// 错误控制
func safeHandler(fn http.HandlerFunc) http.HandlerFunc {
	// 闭包
	return func(w http.ResponseWriter, r *http.Request) {
		// 最后执行
		defer func() {
			// Painc用法是：用于抛出错误。
			// Recover()用法是：将Recover()写在defer中，并且在可能发生panic的地方之前，
			// 先调用此defer的东西（让系统方法域结束时，有代码要执行。）当程序遇到panic的时候（当然，也可以正常的调用出现的异常情况），
			// 系统将跳过后面的代码，进入defer，如果defer函数中recover()，则返回捕获到的panic的值。
			if e, ok := recover().(error); ok {
				http.Error(w, e.Error(), http.StatusInternalServerError)
				// 或者输出自定义的 50x 错误页面
				// w.WriteHeader(http.StatusInternalServerError)
				// renderHtml(w, "error", e)
				// logging
				log.Println("WARN: panic in %v. - %v", fn, e)
				log.Println(string(debug.Stack()))
			}
		}()
		fn(w, r)
	}
}

// 静态资源访问
func staticDirHandler(mux *http.ServeMux, prefix string, staticDir string, flags int) {
	// prefix: /assets/
	// /assets/的文件会自动去拿
	mux.HandleFunc(prefix, func(w http.ResponseWriter, r *http.Request) {

		// path截位截掉prefix
		file := staticDir + r.URL.Path[len(prefix)-1:]

		// 两个1则1，有0则0
		if (flags & ListDir) == 0 {
			if exists, _ := isExists(file); !exists {
				// 404
				http.NotFound(w, r)
				return
			}
		}
		// 可以将服务端的一个文件内容读写到
		// http.Response-Writer 并返回给请求来源的 *http.Request 客户端
		http.ServeFile(w, r, file)
	})
}

func main() {
	mux := http.NewServeMux()
	staticDirHandler(mux, "/assets/", "./public", 0)
	mux.HandleFunc("/", safeHandler(listHandler))
	mux.HandleFunc("/view", safeHandler(viewHandler))
	mux.HandleFunc("/upload", safeHandler(uploadHandler))
	err := http.ListenAndServe(":8888", mux)
	if err != nil {
		log.Fatal("ListenAndServe: ", err.Error())
	}
}
